/*  
 *  Copyright 2006 Paul Jack.
 *
 *  This file is part of the Dex compiler suite. 
 *  
 *  Dex is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License as published by the
 *  Free Software Foundation; either version 2 of the License, or (at your
 *  option) any later version.
 *  
 *  Dex is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 *  Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */
package dex.compiler.checker.type;


import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import dex.compiler.model.base.Identifiers;
import dex.compiler.model.definition.ClassDef;
import dex.compiler.model.definition.Definition;
import dex.compiler.model.definition.Record;
import dex.compiler.model.definition.TypedName;
import dex.compiler.model.definition.Unit;
import dex.compiler.model.definition.UserType;
import dex.compiler.model.program.NameSearcher;
import dex.compiler.model.program.Path;
import dex.compiler.model.program.Program;
import dex.compiler.model.program.Programs;
import dex.compiler.model.program.Searcher;
import dex.compiler.model.type.ArrayTypeNode;
import dex.compiler.model.type.BasicTypeName;
import dex.compiler.model.type.BasicTypeNode;
import dex.compiler.model.type.FunctionTypeNode;
import dex.compiler.model.type.TypeNode;
import dex.compiler.problem.Reporter;
import dex.misc.Augment;
import dex.misc.Cache;
import dex.misc.MapCache;


/**
 *  Manager for type objects.  This class ensures that all instances of Type
 *  objects are unique.
 */
public class TypeManager {
	
	
	/**
	 * Augment field for associating a basic type name with its type.
	 */
	final private static Augment<BasicTypeName,Type> TYPE = Augment.create(BasicTypeName.class, Type.class);
	
	
	/**
	 * The built-in types.
	 */
	final private static Map<String,Type> BUILT_IN;
	
	
	/**
	 * Populates the map of built-in types.
	 */
	static {
		BUILT_IN = new HashMap<String,Type>();
		BUILT_IN.put("boolean", BooleanType.INSTANCE);
		BUILT_IN.put("byte", IntegerType.SIGNED_8);
		BUILT_IN.put("char", IntegerType.SIGNED_16);
		BUILT_IN.put("int", IntegerType.SIGNED_32);
		BUILT_IN.put("long", IntegerType.SIGNED_64);
		BUILT_IN.put("ubyte", IntegerType.UNSIGNED_8);
		BUILT_IN.put("uchar", IntegerType.UNSIGNED_16);
		BUILT_IN.put("uint", IntegerType.UNSIGNED_32);
		BUILT_IN.put("ulong", IntegerType.UNSIGNED_64);
		
	}


	/**
	 * Cache of non-primitive types.
	 */
	private Cache<Type> typeCache;


	/**
	 * The program being checked.
	 */
	private Program program;

	
	/**
	 * Used to reports errors and warnings.
	 */
	private Reporter reporter;
	

	/**
	 * The class hierarchy.
	 */
	private Hierarchy hierarchy;


	/**
	 * Constructs a new TypeManager.
	 */
	public TypeManager(Program program, Reporter reporter) {
		if (program == null) {
			throw new NullPointerException("Pointer may not be null");
		}
		if (reporter == null) {
			throw new NullPointerException("Reporter may not be null");
		}
		this.program = program;
		this.reporter = reporter;
		
		this.typeCache = new MapCache<Type>();
		this.hierarchy = new Hierarchy(reporter);
	}


	/**
	 * Converts a BasicTypeNode into a BasicType.
	 * 
	 * @param context  the unit in which the node appeared
	 * @param n  the node to convert
	 * @return   the converted node
	 */
	private Type basic(Unit context, BasicTypeNode n) {
		Type type = n.getName().get(TYPE);
		if (type != null) {
			return type;
		}
		
		type = BUILT_IN.get(n.getName().toString());
		if (type != null) {
			n.getName().set(TYPE, type);
			return type;
		}
		
		Searcher searcher = new NameSearcher(UserType.class, n.getName().toString());
		List<Path> paths = Programs.search(program, context, searcher);
		int size = paths.size();
		if (size == 0) {
			reporter.error(TypeProblems.MISSING_TYPE, n.getPlace(), n.getName().toString());
			n.getName().set(TYPE, ErrorType.INSTANCE);
			return ErrorType.INSTANCE;
		}
		if (size > 1) {
			reporter.error(TypeProblems.AMBIGUOUS_TYPE, n.getPlace(), n.getName().toString(), paths);
			n.getName().set(TYPE, ErrorType.INSTANCE);
			return ErrorType.INSTANCE;
		}
		
		Path path = paths.get(0);
		if (!Programs.checkVisibility(context, path)) {
			reporter.error(TypeProblems.INVISIBLE_TYPE, n.getPlace(), path.qualified());
		}
		
		String fullName = path.qualified();
		MemberType mt = new MemberType(this, n.isLocal(), fullName, path.getDefinition() instanceof Record);
		mt = typeCache.cache(mt);
		if (path.getDefinition() instanceof ClassDef) {
			hierarchy.determineSuperclass(path);
		}

		n.getName().set(TYPE, mt);
		return mt;
	}


	/**
	 * Converts a FunctionTypeNode into a FunctionType.
	 * 
	 * @param context  the unit in which the node appeared
	 * @param n  the node to convert
	 * @return   the converted node
	 */
	private FunctionType function(Unit context, FunctionTypeNode n) {
		Type returnType = toType(context, n.getReturnType());
		List<Type> parameters = new ArrayList<Type>(n.getParameterTypes().size());
		for (TypeNode node: n.getParameterTypes()) {
			parameters.add(toType(context, node));
		}
		FunctionType ft = new FunctionType(this, returnType, parameters);
		return typeCache.cache(ft);
	}


	/**
	 * Converts an ArrayTypeNode to an ArrayType.
	 * 
	 * @param context  the unit in which the node appeared
	 * @param n  the node to convert
	 * @return   the converted node
	 */
	private ArrayType array(Unit context, ArrayTypeNode n) {
		ArrayType at = new ArrayType(this, toType(context, n.getElementType()));
		return typeCache.cache(at);
	}


	/**
	 * Converts a TypeNode into a Type.
	 * 
	 * @param n  the node to convert
	 * @return   the converted node
	 */
	public Type toType(Unit context, TypeNode n) {
		if (n instanceof BasicTypeNode) {
			return basic(context, (BasicTypeNode)n);
		}
		if (n instanceof ArrayTypeNode) {
			return array(context, (ArrayTypeNode)n);
		}
		if (n instanceof FunctionTypeNode) {
			return function(context, (FunctionTypeNode)n);
		}
		throw new AssertionError();
	}
	
	
	/**
	 * Returns true if the first class is a subclass of the second.
	 * 
	 * @param cls    the subclass to check
	 * @param superclass  the superclass to check
	 * @return  true if subclass is actually a subclass of superclass
	 */
	public boolean isSubclass(Type cls, Type superclass) {
		if (!(cls instanceof MemberType)) {
			return false;
		}
		if (!(superclass instanceof MemberType)) {
			return false;
		}
		MemberType mt1 = (MemberType)cls;
		MemberType mt2 = (MemberType)superclass;
		if (mt1.isRecord() || mt2.isRecord()) {
			return false;
		}
		String className = mt1.getName();
		String superName = mt2.getName();
		return hierarchy.isSubclass(className, superName);
	}


	/**
	 * Converts a list of TypeNodes to a list of Types.
	 * 
	 * @param nodes  the type nodes to convert
	 * @return  the types
	 */
	public List<Type> toTypes(Unit context, Collection<TypeNode> nodes) {
		List<Type> result = new ArrayList<Type>(nodes.size());
		for (TypeNode n: nodes) {
			result.add(toType(context, n));
		}
		return result;
	}
	
	
	/**
	 * Returns the function type with the given return and parameter types.
	 * 
	 * @param returnType  the return type for the function type
	 * @param parameterTypes  the parameter types for the function type
	 * @return  the function type
	 */
	public FunctionType getFunctionType(Type returnType, List<Type> parameterTypes) {
		FunctionType result = new FunctionType(this, returnType, parameterTypes);
		return typeCache.cache(result);
	}
	
	
	public MemberType getMemberType(Path path) {//boolean local, String name, boolean record) {
		Definition def = path.getDefinition();
		String name = path.getUnit().getName() + ":" + def.getName();
		if (def instanceof ClassDef) {
			hierarchy.determineSuperclass(path);
		}
		return typeCache.cache(new MemberType(this, false, name, def instanceof Record));
	}
	
	/**
	 * Returns the type of string literals.
	 * 
	 * @return  the type of string literals
	 */
	public Type getStringType() {
		return null; // FIXME
	}
	
	
	/**
	 * Returns the base class for objects that can be thrown
	 * by a throw statement.
	 * 
	 * @return  the ball class
	 */
	public Type getBall() {
		MemberType result = typeCache.cache(new MemberType(this, false, "dex:core:Ball", false));
		hierarchy.determineSuperclass(getDefinition(result));
		return result;
	}

	
	/**
	 * Returns the definition for a member type.
	 * 
	 * @return  the definition
	 */
	public Path getDefinition(MemberType mt) {
		return getDefinition(mt.getName());
	}

	
	private Path getDefinition(String name) {
		String unitName = Identifiers.getUnit(name);
		String baseName = Identifiers.getBase(name);
		Unit profile = program.loadProfile(unitName);
		NameSearcher searcher = new NameSearcher(UserType.class, baseName);
		List<Path> found = Programs.search(program, profile, searcher);
		if (found.size() != 1) {
			throw new IllegalStateException();
		}
		return found.get(0);		
	}


	/**
	 * Searches a class for a field or a method.
	 * 
	 * @param classPath  the path to the class to search
	 * @param searcher   the searcher to use to find  
	 * @return  the list of members that match
	 */
	public List<Path> search(Path classPath, Searcher searcher) {
		while (!classPath.qualified().equals(Hierarchy.ROOT)) {
			Program p = classPath.getProgram();
			Unit u = classPath.getUnit();
			ClassDef c = (ClassDef)classPath.getDefinition();
			List<Path> results = Programs.search(p, u, c, searcher);
			if (results.size() > 0) {
				return results;
			}
			String superName = hierarchy.determineSuperclass(classPath);
			classPath = getDefinition(superName);			
		}
		return new ArrayList<Path>();
	}

	
	public Path getSuperclass(Path path) {
		String superclass = hierarchy.determineSuperclass(path);
		String unitName = Identifiers.getUnit(superclass);
		String className = Identifiers.getBase(superclass);
		Unit unit = program.loadProfile(unitName);
		for (UserType ut: unit.getUserTypes()) {
			if (ut.getName().equals(className)) {
				return new Path(program, unit, unit, ut);
			}
		}
		return null;
	}


	public Map<String,Type> fields(Path path) {
		Map<String,Type> result = new LinkedHashMap<String,Type>();
		addFields(result, path);
		if (path.getDefinition() instanceof Record) {
			return result;
		}

		while (!path.qualified().equals(Hierarchy.ROOT)) {
			path = getSuperclass(path);
			addFields(result, path);
		}
		return result;
	}
	
	
	private void addFields(Map<String,Type> fields, Path path) {
		UserType ut = (UserType)path.getDefinition();
		for (TypedName tn: ut.getFields()) {
			// FIXME: Report problem if duplicative field
			fields.put(tn.getName(), toType(path.getUnit(), tn.getType()));
		}
	}
	
	
	/**
	 * Returns the program used to load units for verifying
	 * the existence of types.
	 * 
	 * @return  the program
	 */
	public Program getProgram() {
		return program;
	}
}
